openSUSE:WebYaST Testing/Model Encapsulation

Jump to: navigation, search

Model encapsulation when writing tests

For example, if you have a SoftwareService resource class, which reads the product files in /etc/zypp/services.d and exposes them as a RESTful resource, you could create an index controller like:

def show
  # get all files
  Dir.glob("/etc/zypp/services.d/*").each ...
  # parse ini file, make a hash object
  hash = INI.parse(files)
  render hash.to_xml
end

You could test the controller in the controller test class by stubbing what Dir returns when called with that directory, and what File reads when called with those paths.

Dir.stubs(:glob).with("/etc/zypp/services.d/*).returns(filelist)
# stub File.read to return a hardcoded ini file
...

See how it gets complicated? You really don't want that in the controller test. Because the controller test should be focused in the workflow and not in grabbing the data. You should test how the controller *reacts* to certain data and exceptions. Instead, create a SoftwareService model, with the typical find, save, etc methods, and allow to construct it from its attributes.

Then in the controller class, mock

  s1 = SoftwareService.new(attrs)
  s2 = SoftwareService.new(attrs)
  SoftwareService.stubs(:find_all).returns([s1, s2])

And now you can test your controller without worrying how the data is actually read. You can also mock SoftwareService to throw exceptions on certain values and see how your controller reacts.

Then you need to create a new test for the SoftwareService class. In this test you can mock the Dir class to return a fixed filename list when Dir.glob is called with the services.d path.

Then you need to make the decision if you mock the File class to return something fix when constructed with one of those fixed paths. For example you can mock File.new to return an StringIO object constructed from some fixed INI-style string (which acts like a normal file, as it also inherits IO), but only when called with the fixed paths you know.

Dir.stubs(:glob).with("/etc/zypp/services.d/*").returns([ "/etc/zypp/services.d/service1.service", "/etc/zypp/services.d/service2.service"])
Dir.glob("/etc/zypp/services.d/*")
=> ["/etc/zypp/services.d/service1.service", "/etc/zypp/services.d/service2.service"]

The file does not exist, so any attempt to open it will fail.

File.new("/etc/zypp/services.d/service2.service")
Errno::ENOENT: No such file or directory - /etc/zypp/services.d/service2.service

We can stub File.new to return a String based IO object, if we know that our code will only do operations like read.

File.stubs(:new).with("/etc/zypp/services.d/service2.service").returns(StringIO.new("[service]\nattr1=val1\n"))

Now you can read the file even if it does not exist, you will be dealing with a StringIO object, constructed from a fixed string we want to test with:

f = File.new("/etc/zypp/services.d/service2.service")
=> #<StringIO:0x7fa223f10a28>
f.read
=> "[service]\nattr1=val1\n"

But then we will need to mock File for every SoftwareService method that reads the files. Instead, you can move the reading code to a separate method content_for(path), mock that one to return a fixed INI String when testing any method of SoftwareService, and additionally add a test to test content_for(path) that will be the only test mocking the file. See the pattern?

  • You have various methods that to be tested require to mock something complex, like ComplexClass
  • You move the code depending on File to a separate method read_data (fictional name) which talks to ComplexClass
  • You test all methods by mocking the read_data method to return a simple String, Array, etc.
  • You add an additional test to the testcase that tests the new read_data method, and only here you mock ComplexClass

That way you simplify your testcase without reducing coverage.